/*

 Archivo: "cmdline.cpp"

 Procesamiento de opciones en la línea de comando.
 Esta versión provee automáticamente "--help" y "--version".
 No admite parámetros no opción.

 Primera versión: 1-4-2012

*/


#include <cstdlib>
#include <limits>
#include <string>
#include <iostream>
#include <sstream>
#include <locale>

#include "globals.h"
#include "cmdline.h"


#define DOUBLE_DASH "--"
#define DASH        '-'


/* ========================= ProgramVersion - Begin ========================== */
// Tipo para versión del programa.

std::ostream& operator<<(std::ostream &os, const ProgramVersion &vr)
{
    return (os << vr.major_ << '.' << vr.minor_ << '.' << vr.build_);
}

/* ========================= ProgramVersion - End ========================== */


/* ========================= CmdOption - Begin ========================== */
// Opción de línea de comando.

CmdLine::CmdOption::CmdOption(unsigned int argcount, const std::string &shortname,
                              const std::string &longname, const Arguments_Vector_t &defvalues,
                              const std::string &helptext, p_parse_argument_function_t fparse,
                              bool allowrepeat):
    arg_count_(argcount), short_name_(shortname), long_name_(longname), def_values_(defvalues),
    help_text_(helptext), parse_(fparse), allow_repeat_(allowrepeat), ready_(false)
{
}

// Constructor de opción completa sin o con un solo argumento.
CmdLine::CmdOption::CmdOption(unsigned int argcount, const std::string &shortname,
                              const std::string &longname, const std::string &defvalue,
                              const std::string &helptext, p_parse_argument_function_t fparse,
                              bool allowrepeat):
    arg_count_(argcount), short_name_(shortname), long_name_(longname), def_values_(),
    help_text_(helptext), parse_(fparse), allow_repeat_(allowrepeat), ready_(false)

{
    // Si hay un argumento por omisión lo agrego.
    if (!defvalue.empty())
        def_values_.push_back(defvalue);
}

// Constructor de opción usado en la búsqueda.
CmdLine::CmdOption::CmdOption(const std::string &shortname, const std::string &longname):
    arg_count_(0), short_name_(shortname), long_name_(longname), def_values_(), help_text_(),
    parse_(NULL), allow_repeat_(false), ready_(false)
{
}

// Destructor.
CmdLine::CmdOption::~CmdOption()
{
}

// Llena una cola con los parámetros por omisión de la opción.
void CmdLine::CmdOption::Fill_Default_Args(Parameters_Queue_t &pars) const
{
    for (size_t i = 0; i < def_values_.size(); i++)
        pars.push(def_values_[i]);
}

// Devuelve un string con
// todos los argumentos
// por omisión.
const std::string CmdLine::CmdOption::Str_Default_Args() const
{
    std::string aux;

    for (size_t i = 0; i < def_values_.size(); i++)
    {
        if (i > 0)
            aux += " ";

        aux += def_values_[i];
    }

    return aux;
}

// Devuelve el nombre de la opción.
const std::string CmdLine::CmdOption::Option() const
{
    if (!this->short_name_.empty())
        return (DASH + short_name_);
    else
        return (DOUBLE_DASH + long_name_);
}

/* ========================= CmdOption - Begin ========================== */


/* ========================= CompShortOption - Begin ========================== */
// Clase función para comparación de Opciones para el std::set.

// Operador llamada usado para la comparación,
// devuelve true si (lopt < ropt). [OPERADOR]
bool CmdLine::CompShortOption::operator() (const PCmdOption& lopt, const PCmdOption& ropt) const
{
    return (lopt->short_name_ < ropt->short_name_);
}

/* ========================= CompShortOption - End ========================== */


/* ========================= CompLongOption - Begin ========================== */
// Clase función para comparación de Opciones para el std::set.

// Operador llamada usado para la comparación,
// devuelve true si (lopt < ropt). [OPERADOR]
bool CmdLine::CompLongOption::operator() (const PCmdOption& lopt, const PCmdOption& ropt) const
{
    return (lopt->long_name_ < ropt->long_name_);
}

/* ========================= CompLongOption - End ========================== */


/* ========================= CmdLine - Begin ========================== */
// Clase para parseo de la línea de comandos.

/* ***** Constructores y destructores - Begin ***** */

// El constructor por defecto CmdLine::CmdLine(), es
// privado, para evitar construir parsers sin parámetros.
CmdLine::CmdLine() :
    parsed_(false), options_(), set_so_poptions_(), set_lo_poptions_(),
    program_name_(""), program_description_(""),
    program_version_(), parse_non_option_arg_(NULL),
    help_text_()
{
}

// Constructor que toma el nombre, la descripción, la versión del programa
// y un puntero a la función que procesa los argumentos que no son opciones.
CmdLine::CmdLine(const std::string progname,
                 const std::string progdescription,
                 const ProgramVersion &progversion,
                 p_parse_non_option_function_t parsenonoptionarg) :
    parsed_(false), options_(), set_so_poptions_(), set_lo_poptions_(),
    program_name_(progname), program_description_(progdescription),
    program_version_(progversion),
    parse_non_option_arg_(parsenonoptionarg),
    help_text_()
{
}

// Constructor copia.
// Dudo que se use, no tiene utilidad, pero por seguridad se define.
CmdLine::CmdLine(const CmdLine &cmdr) :
    parsed_(cmdr.parsed_), options_(cmdr.options_), set_so_poptions_(),
    set_lo_poptions_(), program_name_(cmdr.program_name_),
    program_description_(cmdr.program_description_),
    program_version_(cmdr.program_version_),
    parse_non_option_arg_(cmdr.parse_non_option_arg_), help_text_()
{
    // Lleno los set ordenados con punteros
    // a las opciones de la propia lista.
    for (Options_list_t::iterator it = options_.begin();
            it != options_.end(); it++)
    {
        CmdOption &opt = *it;

        set_so_poptions_.insert(&opt);
        set_lo_poptions_.insert(&opt);
    }
}

// Destructor.
CmdLine::~CmdLine()
{
}

/* ***** Constructores y destructores - End ***** */

/* ***** Métodos públicos - Begin ***** */

// Procesa una opción consumiendo parámetros de la cola.
void CmdLine::Do_Option(const std::string &opt, Parameters_Queue_t &args, bool longopt)
{
    // String que guarda el prefijo adecuado "-"\"--".
    std::string sprefix;

    // Parámetros para la función que parsea la opción.
    Parameters_Queue_t funcargs;


    // Puntero a la opción hallada.
    CmdOption* pOption = NULL;

    // Busco la opción correspondiente en el set.
    if (longopt)
    {
        // Iterador para la búsqueda de la opción.
        POptions_lo_set_t::iterator it;

        CmdOption auxOptn("", opt);

        // Busco la opción larga en el set ordenado.
        it = set_lo_poptions_.find(&auxOptn);

        // Si se encuentra tomo el puntero.
        if (it != set_lo_poptions_.end())
            pOption = *it;

        sprefix = DOUBLE_DASH;
    }
    else
    {
        // Iterador para la búsqueda de la opción.
        POptions_so_set_t::iterator it;

        CmdOption auxOptn(opt, "");

        // Busco la opción larga en el set ordenado.
        it = set_so_poptions_.find(&auxOptn);

        // Si se encuentra tomo el puntero.
        if (it != set_so_poptions_.end())
            pOption = *it;

        sprefix = DOUBLE_DASH;
    }

    // Verifico si se encontró la opción.
    if (pOption == NULL)
    {
        // Chequeo si se trata de help o version.
        if ((longopt && (opt == LONG_HELP_OPTION)) ||
                (!longopt && (opt == SHORT_HELP_OPTION)))
        {
            // Muestra la ayuda y sale del programa.
            // Ignora toda otra posible opción y\o argumento.
            Do_Usage();
            exit(ERROR_NUMBER_SUCCESS);
        }
        else if ((longopt && (opt == LONG_VERSION_OPTION)) ||
                 (!longopt && (opt == SHORT_VERSION_OPTION)))
        {
            // Muestra la version y sale del programa.
            // Ignora toda otra posible opción y\o argumento.
            Do_Version();
            exit(ERROR_NUMBER_SUCCESS);
        }

        // Error: opción no reconocida. Imprimimos un mensaje
        // de error, y finalizamos la ejecución del programa.
        std::cerr << UNKNOWN_OPTION
                  << sprefix
                  << opt
                  << DOT
                  << std::endl;

        // Muestra la ayuda y sale del programa.
        Do_Usage();
        exit(ERROR_NUMBER_COMMAND_PARSING);
    }

    // Verifico si la opción está repetida.
    if (pOption->ready_ && (!pOption->allow_repeat_))
    {
        // Error: opción repetida. Imprimimos un mensaje
        // de error, y finalizamos la ejecución del programa.
        std::cerr << REPEATED_OPTION
                  << sprefix
                  << opt
                  << DOT
                  << std::endl;

        // Muestra la ayuda y sale del programa.
        Do_Usage();
        exit(ERROR_NUMBER_COMMAND_PARSING);
    }

    // Marcamos esta opción como usada en
    // forma explícita, para evitar tener
    // que inicializarla con el valor por
    // defecto.
    pOption->ready_ = true;

    // Verifico si la opción tiene argumentos.
    if (pOption->arg_count_ > 0)
    {
        // Como se trata de una opción
        // con argumentos, verificamos que
        // los mismos hayan sido provistos.
        if (args.size() < pOption->arg_count_)
        {
            std::cerr << OPTION_REQUIRES
                      << pOption->arg_count_ << ' '
                      << ARGUMENTS
                      << sprefix
                      << opt
                      << DOT
                      << std::endl;

            // Muestra la ayuda y sale del programa.
            Do_Usage();
            exit(ERROR_NUMBER_COMMAND_PARSING);
        }

        // Opción con argumento.
        pOption->parse_(args);

        return;
    }
    else
    {

        // Copio en la cola temporal los argumentos
        // de esta opción.
        while (funcargs.size() < pOption->arg_count_)
        {
            funcargs.push(args.front());
            args.pop();
        }


        // Opción sin argumento.
        pOption->parse_(funcargs);

        return;
    }
}

// Método que muestra el help del programa.
void CmdLine::Do_Usage() const
{
    // String con las opciones default.
    std::string def_options;

	// Largo de la opción más laga a mostrar.
	size_t max_long_opt = 0, long_opt;

    // Determina el string de las
    // opciones por default mostrado en el help.
    for (Options_list_t::const_iterator it = options_.begin();
            it != options_.end(); it++)
    {
        if (it->def_values_.size() > 0)
            def_options = def_options + SPACE + DASH +
                          it->short_name_ + SPACE +
                          it->Str_Default_Args();

		// Calculo el largo de la opción a imprimir en la ayuda.
		long_opt = it->short_name_.length() + it->long_name_.length();

		if (max_long_opt < long_opt)
			max_long_opt = long_opt;
    }

    // Variables que indican si debe imprimir el help para
    // las opciones auto generadas.
    bool hide_help_opt = false;
    bool hide_version_opt = false;

    // Imprimo un help que se arma en forma dinámica con
    // el contenido de las opciones en el set.
    // También se muestra las opciones por default,
    // las cuales ya se determinaron también
    // en forma dinámica en el paso anterior.

    std::cerr
        << std::endl << USAGE << program_name_
        << OPTION_ELIPSIS << std::endl
        <<  program_description_ << DEFAULT_OPT_DESCRIPTION
        << program_name_ << def_options
        << std::endl << std::endl;

    // Recorro todas las opciones, imprimiendo
    // la ayuda correspondiente.
    for (Options_list_t::const_iterator it = options_.begin();
            it != options_.end(); it++)
    {
        if (!(it->short_name_.empty()))
        {
            std::cerr << DASH
                      << it->short_name_;

            if (!(it->long_name_.empty()))
                std::cerr << DESC_OPT_PREFIX;
        }

        if (!(it->long_name_.empty()))
        {
            if (it->short_name_.empty())
                std::cerr << DOUBLE_DASH;

            std::cerr << it->long_name_;
        }

        std::cerr
            << std::string(max_long_opt - it->short_name_.length() - 
			it->long_name_.length(), ' ') + "\t"
            << it->help_text_
            << std::endl;

        // Si se sobreescribió el significado de "-h"
        // no se debe mostrar la opción en la ayuda.
        hide_help_opt = (hide_help_opt ||
                         (it->short_name_ == SHORT_HELP_OPTION) ||
                         (it->long_name_ == LONG_HELP_OPTION));

        // Si se sobreescribió el significado de "-v"
        // no se debe mostrar la opción en la ayuda.
        hide_version_opt = (hide_version_opt ||
                            (it->short_name_ == SHORT_VERSION_OPTION) ||
                            (it->long_name_ == LONG_VERSION_OPTION));
    };

    // Imprimo un salto de línea extra antes de
    // imprimir el help de "-h" y\o "v", si
    // corresponde.
    if (!hide_help_opt || !hide_version_opt)
        std::cerr << std::endl;

    // Si corresponde muestro la ayuda de la opción "-h".
    if (!hide_help_opt)
        std::cerr
            << DASH
            << SHORT_HELP_OPTION
            << DESC_OPT_PREFIX
            << LONG_HELP_OPTION
            << std::string(max_long_opt - std::string(SHORT_HELP_OPTION).length() - 
			std::string(LONG_HELP_OPTION).length(), ' ') + "\t"
            << HELP_HELP_OPTION
            << std::endl;

    // Si corresponde muestro la ayuda de la opción "-v".
    if (!hide_version_opt)
        std::cerr
            << DASH
            << SHORT_VERSION_OPTION
            << DESC_OPT_PREFIX
            << LONG_VERSION_OPTION
            << std::string(max_long_opt - std::string(SHORT_VERSION_OPTION).length() - 
			std::string(LONG_VERSION_OPTION).length(), ' ') + "\t"
            << HELP_VERSION_OPTION
            << std::endl;
}

// Método que muestra la versión del programa.
void CmdLine::Do_Version() const
{
    // Imprimo la versión del programa.
    std::cerr << program_name_ << VERSION_TEXT << program_version_ << DOT <<std::endl;
}

// Valida un nombre de opción.
bool CmdLine::Validate_Name(const std::string& name)
{
    // Locale usado para la
    // validación de caractres.
    std::locale loc;

    // Verifico que el nombre no sea vació
    // ni comience con un dash.
    if (name.empty() || (name[0] == DASH))
        return false;

    // Verifico que solo contenga caracteres
    // imprimibles alfanuméricos.
    for (size_t i = 0; i < name.length(); i++)
    {
        if (!std::isalnum(name[i], loc))
            return false;
    }

    return true;
}

/* ***** Métodos públicos - End ***** */

/* ***** Métodos públicos - Begin ***** */

// Método que agrega una nueva opción.
// Retorna 0 en caso de que la opción
// se haya agregado correctamente y un
// código de error mayor o igual a
// 1 en caso contrario.
// Los parámetros son:
// Cantidad de argumentos, el nombre corto,
// el nombre largo, los valores por omisión,
// el texto descriptivo, la función de parseo y
// si se permite que la opción se repita.
unsigned int CmdLine::Add_Option(unsigned int argcount, std::string short_name,
                                 std::string long_name, const Arguments_Vector_t &def_values,
                                 std::string help_text, p_parse_argument_function_t parse,
                                 bool allow_repeat)
{
    // Verifico la validez de los nombres
    if (((!short_name.empty()) && (!Validate_Name(short_name))) ||
            ((!long_name.empty()) && (!Validate_Name(long_name))) ||
            (short_name.empty() && long_name.empty()))
        return 1;

    // Veirifico que el resto de los parámetros sean válidos.
    if ((parse == NULL) || (argcount != def_values.size()))
        return 1;

    // Creo la nueva opción.
    CmdOption op(argcount, short_name, long_name,
                 def_values, help_text, parse, allow_repeat);


    // Tomo la dirección de la opción creada.
    PCmdOption popt = &op;

    // Busco la opción por nombre corto,
    // de encontrala devuelvo un error.
    if (!short_name.empty())
    {
        POptions_so_set_t::const_iterator it = set_so_poptions_.find(popt);

        if (it != set_so_poptions_.end())
            return 2;
    }

    // Busco la opción por nombre largo,
    // de encontrala devuelvo un error.
    if (!long_name.empty())
    {
        POptions_lo_set_t::const_iterator it = set_lo_poptions_.find(popt);

        if (it != set_lo_poptions_.end())
            return 3;
    }

    // Inserto la opción en la lista (una copia).
    options_.push_back(op);

    // Tomo la dirección del elemento insertado en la lista.
    popt = &(options_.back());

    // Inserto un puntero a la opción
    // en el set ordenado por nombre corto.
    POptions_so_set_pair_t pair1 =
        set_so_poptions_.insert(popt);

    // Si por alguna razón no
    // se pudo insertar retorno un error.
    if (!pair1.second)
        return 4;

    // Inserto un puntero a la opción
    // en el set ordenado por nombre largo.
    POptions_lo_set_pair_t pair2 =
        set_lo_poptions_.insert(popt);

    // Si por alguna razón no
    // se pudo insertar retorno un error.
    if (!pair2.second)
    {
        // Borro el puntero en el primer set.
        set_so_poptions_.erase(pair1.first);

        return 5;
    }

    // La opción se insertó sin problemas.
    return 0;
}

// Método que agrega una nueva opción.
// Retorna 0 en caso de que la opción
// se haya agregado correctamente y un
// código de error mayor o igual a
// 1 en caso contrario.
// Los parámetros son:
// Cantidad de argumentos, el nombre corto,
// el nombre largo, el valor por omisión,
// el texto descriptivo, la función de parseo y
// si se permite que la opción se repita.
unsigned int CmdLine::Add_Option(unsigned int argcount, std::string short_name,
                                 std::string long_name, std::string def_value,
                                 std::string help_text, p_parse_argument_function_t parse,
                                 bool allow_repeat)
{
    Arguments_Vector_t defargs;

    if (!def_value.empty())
        defargs.push_back(def_value);

    // Llamo a la versión que toma un vector de argumentos default.
    return Add_Option(argcount, short_name, long_name, defargs,
                      help_text, parse, allow_repeat);
}

// Método que parsea la línea de comandos.
void CmdLine::Parse(int argc, char * const argv[])
{
    // Si ya se llamó la función

    // salimos sin hacer nada.
    // Otra opción sería terminar el
    // programa.
    if (parsed_)
        return;

    // Indico que ya se llamo a esta
    // función.
    parsed_ = true;

    // Si no se agregaron opciones,
    // salimos sin hacer nada.
    // Otra opción sería terminar el
    // programa.
    if (options_.size() == 0)
        return;

    // Cola de parámetros.
    Parameters_Queue_t parameters;

    // Guardo los parámetros en una cola
    // de parámetros.
    for (int i = 1; i < argc; ++i)
        parameters.push(argv[i]);

    // Proceso todos los parámetros
    // hasta cosumirlos todos o que
    // se produzca un error.
    while (parameters.size() > 0)
    {
        // Tomo el primer parámetro en la cola.
        std::string par = parameters.front();

        // Elimino el parámetro consumido.
        parameters.pop();

        // Verifico si el parámetro es o no
        // una opción.
        if ((par[0] != DASH) || (par.size() == 1))
        {
            // Verifico si el programa
            // acepta parámetros no opción.
            if (parse_non_option_arg_ != NULL)
            {
                // Proceso el parámetro no opción
                // y continuo con el siguiente
                // parémetro de la línea de comando.
                parse_non_option_arg_(par);
                continue;
            }
            else
            {
                // El programa no acepta parámetros
                // que no son opción.
                std::cerr << INVALID_NON_OPTION_ARGUMENT
                          << par
                          << DOT
                          << std::endl;

                // Muestra la ayuda y sale del programa.
                Do_Usage();
                exit(ERROR_NUMBER_COMMAND_PARSING);
            };
        }

        // Usamos "--" para marcar el fin de las
        // opciones; todos los argumentos que puedan
        // estar a continuación no son interpretados
        // como opciones.
        if ((par.size() == 2) && (par[1] == DASH))
            break;

        // Finalmente, vemos si se trata o no de una
        // opción larga; y llamamos al método que se
        // encarga con los parámetros adecuados
        // en cada caso.

        // Opción sin los dashes.
        std::string sopt;

        if (par[1] == DASH)
            sopt = par.substr(2);
        else
            sopt = par.substr(1);


        Do_Option(sopt, parameters, (par[1] == DASH));
    }

    // Aseguro que la cola quede vacía.
    while (parameters.size() > 0)
        parameters.pop();

    // Procesamos aquellas opciones que,
    // (1) no hayan figurado explícitamente en la línea
    // de comandos, y (2) tengan valor por defecto.
    for (Options_list_t::iterator it = options_.begin();
            it != options_.end(); it++)
    {
        // Verifico si ya se procesó.
        if (it->ready_)
            continue;

        // Procesada.
        it->ready_ = true;

        if (it->def_values_.size() == 0)
        {
            // La opción no fué pasada por
            // la línea de comando y no
            // proporcionaron
            // argumentos por omisión
            // para la misma, esto es un error.

            // Al programa le falta una opción..
            std::cerr << MISSING_PARAMETER
                      << it->Option()
                      << DOT
                      << std::endl;

            // Muestra la ayuda y sale del programa.
            Do_Usage();
            exit(ERROR_NUMBER_COMMAND_PARSING);
        }

        // Cargo en la cola los argumentos por omisión.
        it->Fill_Default_Args(parameters);

        // Los proceso.
        it->parse_(parameters);
    }
}

/* ***** Métodos públicos - End ***** */

/* ***** Operadores - Begin ***** */

// Operador asignación. [OPERADOR]
CmdLine& CmdLine::operator=(const CmdLine &cmdr)
{
    options_ = cmdr.options_;
    program_name_ = cmdr.program_name_;
    program_description_ = cmdr.program_description_;
    program_version_ = cmdr.program_version_;
    parse_non_option_arg_ = cmdr.parse_non_option_arg_;

    // Limpio los sets.
    set_so_poptions_.clear();
    set_lo_poptions_.clear();

    // Lleno los set ordenados con punteros
    // a las opciones de la propia lista.
    for (Options_list_t::iterator it = options_.begin();
            it != options_.end(); it++)
    {
        CmdOption &opt = *it;

        set_so_poptions_.insert(&opt);
        set_lo_poptions_.insert(&opt);
    }

    return *this;
}

/* ***** Operadores - End ***** */

/* ========================= CmdLine - End ========================== */
